/**
 * elancom Corporation copy right 2012 - 2015  
 *   
 * $Header: https://192.168.1.224/svn/elancom/product/elancom-platform/trunk/src/main/java/com/elancom/framework/commons/json/util/TypeUtils.java 1034 2012-07-02 03:09:12Z lanzhouxue $
 * $Id: TypeUtils.java 1034 2012-07-02 03:09:12Z lanzhouxue $
 * $Author: lanzhouxue $
 * $Date: 2012-07-02 11:09:12 +0800 (Mon, 02 Jul 2012) $
 * $Revision: 1034 $
 */
package com.elancom.framework.commons.json.util;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.elancom.framework.commons.json.JSONException;
import com.elancom.framework.commons.json.JSONObject;
import com.elancom.framework.commons.json.annotation.JSONField;
import com.elancom.framework.commons.json.parser.ParserConfig;
import com.elancom.framework.commons.json.parser.deserializer.FieldDeserializer;

/**
 * @author elancom
 */
public class TypeUtils {

	public static final String castToString(Object value) {
		if (value == null) {
			return null;
		}

		return value.toString();
	}

	public static final Byte castToByte(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).byteValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}
			return Byte.parseByte(strVal);
		}

		throw new JSONException("can not cast to byte, value : " + value);
	}

	public static final Character castToChar(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Character) {
			return (Character) value;
		}

		if (value instanceof String) {
			final String strVal = (String) value;

			if (strVal.length() == 0) {
				return null;
			}

			if (strVal.length() != 1) {
				throw new JSONException("can not cast to byte, value : " + value);
			}

			return strVal.charAt(0);
		}

		throw new JSONException("can not cast to byte, value : " + value);
	}

	public static final Short castToShort(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).shortValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}
			return Short.parseShort(strVal);
		}

		throw new JSONException("can not cast to short, value : " + value);
	}

	public static final BigDecimal castToBigDecimal(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof BigDecimal) {
			return (BigDecimal) value;
		}

		if (value instanceof BigInteger) {
			return new BigDecimal((BigInteger) value);
		}

		final String strVal = value.toString();
		if (strVal.length() == 0) {
			return null;
		}

		return new BigDecimal(strVal);
	}

	public static final BigInteger castToBigInteger(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof BigInteger) {
			return (BigInteger) value;
		}

		if (value instanceof Float || value instanceof Double) {
			return BigInteger.valueOf(((Number) value).longValue());
		}

		final String strVal = value.toString();
		if (strVal.length() == 0) {
			return null;
		}

		return new BigInteger(strVal);
	}

	public static final Float castToFloat(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).floatValue();
		}

		if (value instanceof String) {
			final String strVal = value.toString();
			if (strVal.length() == 0) {
				return null;
			}

			return Float.parseFloat(strVal);
		}

		throw new JSONException("can not cast to float, value : " + value);
	}

	public static final Double castToDouble(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).doubleValue();
		}

		if (value instanceof String) {
			final String strVal = value.toString();
			if (strVal.length() == 0) {
				return null;
			}
			return Double.parseDouble(strVal);
		}

		throw new JSONException("can not cast to double, value : " + value);
	}

	public static final Date castToDate(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Calendar) {
			return ((Calendar) value).getTime();
		}

		if (value instanceof Date) {
			return (Date) value;
		}

		long longValue = 0;

		if (value instanceof Number) {
			longValue = ((Number) value).longValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}

			longValue = Long.parseLong(strVal);
		}

		if (longValue <= 0) {
			throw new JSONException("can not cast to Date, value : " + value);
		}

		return new Date(longValue);
	}

	public static final java.sql.Date castToSqlDate(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Calendar) {
			return new java.sql.Date(((Calendar) value).getTimeInMillis());
		}

		if (value instanceof java.sql.Date) {
			return (java.sql.Date) value;
		}

		if (value instanceof java.util.Date) {
			return new java.sql.Date(((java.util.Date) value).getTime());
		}

		long longValue = 0;

		if (value instanceof Number) {
			longValue = ((Number) value).longValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}

			longValue = Long.parseLong(strVal);
		}

		if (longValue <= 0) {
			throw new JSONException("can not cast to Date, value : " + value);
		}

		return new java.sql.Date(longValue);
	}

	public static final java.sql.Timestamp castToTimestamp(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Calendar) {
			return new java.sql.Timestamp(((Calendar) value).getTimeInMillis());
		}

		if (value instanceof java.sql.Timestamp) {
			return (java.sql.Timestamp) value;
		}

		if (value instanceof java.util.Date) {
			return new java.sql.Timestamp(((java.util.Date) value).getTime());
		}

		long longValue = 0;

		if (value instanceof Number) {
			longValue = ((Number) value).longValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}

			longValue = Long.parseLong(strVal);
		}

		if (longValue <= 0) {
			throw new JSONException("can not cast to Date, value : " + value);
		}

		return new java.sql.Timestamp(longValue);
	}

	public static final Long castToLong(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).longValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}

			return Long.parseLong(strVal);
		}

		throw new JSONException("can not cast to long, value : " + value);
	}

	public static final Integer castToInt(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Integer) {
			return (Integer) value;
		}

		if (value instanceof Number) {
			return ((Number) value).intValue();
		}

		if (value instanceof String) {
			final String strVal = (String) value;
			if (strVal.length() == 0) {
				return null;
			}

			return Integer.parseInt(strVal);
		}

		throw new JSONException("can not cast to int, value : " + value);
	}

	public static final byte[] castToBytes(Object value) {
		if (value instanceof byte[]) {
			return (byte[]) value;
		}

		if (value instanceof String) {
			return Base64.decodeFast((String) value);
		}
		throw new JSONException("can not cast to int, value : " + value);
	}

	public static final Boolean castToBoolean(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Boolean) {
			return (Boolean) value;
		}

		if (value instanceof Number) {
			return ((Number) value).intValue() == 1;
		}

		if (value instanceof String) {
			final String str = (String) value;
			if (str.length() == 0) {
				return null;
			}

			if ("true".equals(str)) {
				return Boolean.TRUE;
			}
			if ("false".equals(str)) {
				return Boolean.FALSE;
			}

			if ("1".equals(str)) {
				return Boolean.TRUE;
			}
		}

		throw new JSONException("can not cast to int, value : " + value);
	}

	public static final <T> T castToJavaBean(Object obj, Class<T> clazz) {
		return cast(obj, clazz, ParserConfig.getGlobalInstance());
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static final <T> T cast(Object obj, Class<T> clazz, ParserConfig mapping) {
		if (obj == null) {
			return null;
		}

		if (clazz == obj.getClass()) {
			return (T) obj;
		}

		if (obj instanceof Map) {
			if (clazz == Map.class) {
				return (T) obj;
			}

			return castToJavaBean((Map<String, Object>) obj, clazz, mapping);
		}

		if (clazz.isArray()) {
			if (obj instanceof Collection) {

				final Collection collection = (Collection) obj;
				int index = 0;
				final Object array = Array.newInstance(clazz.getComponentType(), collection.size());
				for (final Object item : collection) {
					final Object value = cast(item, clazz.getComponentType(), mapping);
					Array.set(array, index, value);
					index++;
				}

				return (T) array;
			}
		}

		if (clazz.isAssignableFrom(obj.getClass())) {
			return (T) obj;
		}

		if (clazz == boolean.class || clazz == Boolean.class) {
			return (T) castToBoolean(obj);
		}

		if (clazz == byte.class || clazz == Byte.class) {
			return (T) castToByte(obj);
		}

		// if (clazz == char.class || clazz == Character.class) {
		// return (T) castToCharacter(obj);
		// }

		if (clazz == short.class || clazz == Short.class) {
			return (T) castToShort(obj);
		}

		if (clazz == int.class || clazz == Integer.class) {
			return (T) castToInt(obj);
		}

		if (clazz == long.class || clazz == Long.class) {
			return (T) castToLong(obj);
		}

		if (clazz == float.class || clazz == Float.class) {
			return (T) castToFloat(obj);
		}

		if (clazz == double.class || clazz == Double.class) {
			return (T) castToDouble(obj);
		}

		if (clazz == String.class) {
			return (T) castToString(obj);
		}

		if (clazz == BigDecimal.class) {
			return (T) castToBigDecimal(obj);
		}

		if (clazz == BigInteger.class) {
			return (T) castToBigInteger(obj);
		}

		if (clazz == Date.class) {
			return (T) castToDate(obj);
		}

		if (clazz == java.sql.Date.class) {
			return (T) castToSqlDate(obj);
		}

		if (clazz == java.sql.Timestamp.class) {
			return (T) castToTimestamp(obj);
		}

		if (clazz.isEnum()) {
			return castToEnum(obj, clazz, mapping);
		}

		if (obj instanceof String) {
			final String strVal = (String) obj;
			if (strVal.length() == 0) {
				return null;
			}
		}

		throw new JSONException("can not cast to : " + clazz.getName());
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static final <T> T castToEnum(Object obj, Class<T> clazz, ParserConfig mapping) {
		try {
			if (obj instanceof String) {
				final String name = (String) obj;
				if (name.length() == 0) {
					return null;
				}

				return (T) Enum.valueOf((Class<? extends Enum>) clazz, name);
			}

			if (obj instanceof Number) {
				final int ordinal = ((Number) obj).intValue();

				final Method method = clazz.getMethod("values");
				final Object[] values = (Object[]) method.invoke(null);
				for (final Object value : values) {
					final Enum e = (Enum) value;
					if (e.ordinal() == ordinal) {
						return (T) e;
					}
				}
			}
		} catch (final Exception ex) {
			throw new JSONException("can not cast to : " + clazz.getName(), ex);
		}

		throw new JSONException("can not cast to : " + clazz.getName());
	}

	@SuppressWarnings("unchecked")
	public static final <T> T cast(Object obj, Type type, ParserConfig mapping) {
		if (obj == null) {
			return null;
		}

		if (type instanceof Class) {
			return cast(obj, (Class<T>) type, mapping);
		}

		if (type instanceof ParameterizedType) {
			return (T) cast(obj, (ParameterizedType) type, mapping);
		}

		if (obj instanceof String) {
			final String strVal = (String) obj;
			if (strVal.length() == 0) {
				return null;
			}
		}

		if (type instanceof TypeVariable) {
			return (T) obj;
		}

		throw new JSONException("can not cast to : " + type);
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static final <T> T cast(Object obj, ParameterizedType type, ParserConfig mapping) {
		final Type rawTye = type.getRawType();

		if (rawTye == List.class || rawTye == ArrayList.class) {
			final Type itemType = type.getActualTypeArguments()[0];

			if (obj instanceof Iterable) {
				final List list = new ArrayList();

				for (final Iterator it = ((Iterable) obj).iterator(); it.hasNext();) {
					final Object item = it.next();
					list.add(cast(item, itemType, mapping));
				}

				return (T) list;
			}
		}

		if (rawTye == Map.class || rawTye == HashMap.class) {
			final Type keyType = type.getActualTypeArguments()[0];
			final Type valueType = type.getActualTypeArguments()[1];

			if (obj instanceof Map) {
				final Map map = new HashMap();

				for (final Map.Entry entry : ((Map<?, ?>) obj).entrySet()) {
					final Object key = cast(entry.getKey(), keyType, mapping);
					final Object value = cast(entry.getValue(), valueType, mapping);

					map.put(key, value);
				}

				return (T) map;
			}
		}

		if (obj instanceof String) {
			final String strVal = (String) obj;
			if (strVal.length() == 0) {
				return null;
			}
		}

		if (type.getActualTypeArguments().length == 1) {
			final Type argType = type.getActualTypeArguments()[0];
			if (argType instanceof WildcardType) {
				return (T) cast(obj, rawTye, mapping);
			}
		}

		throw new JSONException("can not cast to : " + type);
	}

	@SuppressWarnings({ "unchecked" })
	public static final <T> T castToJavaBean(Map<String, Object> map, Class<T> clazz, ParserConfig mapping) {
		try {
			if (clazz == StackTraceElement.class) {
				final String declaringClass = (String) map.get("className");
				final String methodName = (String) map.get("methodName");
				final String fileName = (String) map.get("fileName");
				int lineNumber;
				{
					final Number value = (Number) map.get("lineNumber");
					if (value == null) {
						lineNumber = 0;
					} else {
						lineNumber = value.intValue();
					}
				}

				return (T) new StackTraceElement(declaringClass, methodName, fileName, lineNumber);
			}

			{
				final Object iClassObject = map.get("@type");
				if (iClassObject instanceof String) {
					final String className = (String) iClassObject;

					clazz = (Class<T>) loadClass(className);
				}
			}

			if (clazz.isInterface()) {
				JSONObject object;

				if (map instanceof JSONObject) {
					object = (JSONObject) map;
				} else {
					object = new JSONObject(map);
				}

				return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[] { clazz }, object);
			}

			final Map<String, FieldDeserializer> setters = mapping.getFieldDeserializers(clazz);

			final T object = clazz.newInstance();

			for (final Map.Entry<String, FieldDeserializer> entry : setters.entrySet()) {
				final String key = entry.getKey();
				final Method method = entry.getValue().getMethod();

				if (map.containsKey(key)) {
					Object value = map.get(key);
					value = cast(value, method.getGenericParameterTypes()[0], mapping);
					method.invoke(object, new Object[] { value });
				}
			}

			return object;
		} catch (final Exception e) {
			throw new JSONException(e.getMessage(), e);
		}
	}

	private static Map<String, Class<?>> mappings = new HashMap<String, Class<?>>();
	static {
		mappings.put("byte", byte.class);
		mappings.put("short", short.class);
		mappings.put("int", int.class);
		mappings.put("long", long.class);
		mappings.put("float", float.class);
		mappings.put("double", double.class);
		mappings.put("boolean", boolean.class);
		mappings.put("char", char.class);

		mappings.put("[byte", byte[].class);
		mappings.put("[short", short[].class);
		mappings.put("[int", int[].class);
		mappings.put("[long", long[].class);
		mappings.put("[float", float[].class);
		mappings.put("[double", double[].class);
		mappings.put("[boolean", boolean[].class);
		mappings.put("[char", char[].class);
	}

	public static Class<?> loadClass(String className) {
		if (className == null || className.length() == 0) {
			return null;
		}

		Class<?> clazz = mappings.get(className);

		if (clazz != null) {
			return clazz;
		}

		if (className.charAt(0) == '[') {
			final Class<?> componentType = loadClass(className.substring(1));
			return Array.newInstance(componentType, 0).getClass();
		}

		try {
			clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
			return clazz;
		} catch (final Throwable e) {
			// skip
		}

		try {
			clazz = Class.forName(className);
			return clazz;
		} catch (final Throwable e) {
			// skip
		}

		return clazz;
	}

	public static List<FieldInfo> computeGetters(Class<?> clazz, Map<String, String> aliasMap) {
		final List<FieldInfo> fieldInfoList = new ArrayList<FieldInfo>();

		final Map<String, FieldInfo> fieldInfoMap = new LinkedHashMap<String, FieldInfo>();

		for (final Method method : clazz.getMethods()) {
			final String methodName = method.getName();

			if (Modifier.isStatic(method.getModifiers())) {
				continue;
			}

			if (method.getReturnType().equals(Void.TYPE)) {
				continue;
			}

			if (method.getParameterTypes().length != 0) {
				continue;
			}

			final JSONField annotation = method.getAnnotation(JSONField.class);

			if (annotation != null) {
				if (!annotation.serialize()) {
					continue;
				}

				if (annotation.name().length() != 0) {
					String propertyName = annotation.name();

					if (aliasMap != null) {
						propertyName = aliasMap.get(propertyName);
						if (propertyName == null) {
							continue;
						}
					}

					fieldInfoMap.put(propertyName, new FieldInfo(propertyName, method, null));
					continue;
				}
			}

			if (methodName.startsWith("get")) {
				if (methodName.length() < 4) {
					continue;
				}

				if (methodName.equals("getClass")) {
					continue;
				}

				if (!Character.isUpperCase(methodName.charAt(3))) {
					continue;
				}

				String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);

				final Field field = ParserConfig.getField(clazz, propertyName);
				if (field != null) {
					final JSONField fieldAnnotation = field.getAnnotation(JSONField.class);

					if (fieldAnnotation != null && fieldAnnotation.name().length() != 0) {
						propertyName = fieldAnnotation.name();

						if (aliasMap != null) {
							propertyName = aliasMap.get(propertyName);
							if (propertyName == null) {
								continue;
							}
						}
					}
				}

				if (aliasMap != null) {
					propertyName = aliasMap.get(propertyName);
					if (propertyName == null) {
						continue;
					}
				}

				fieldInfoMap.put(propertyName, new FieldInfo(propertyName, method, field));
			}

			if (methodName.startsWith("is")) {
				if (methodName.length() < 3) {
					continue;
				}

				if (!Character.isUpperCase(methodName.charAt(2))) {
					continue;
				}

				String propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);

				final Field field = ParserConfig.getField(clazz, propertyName);
				if (field != null) {
					final JSONField fieldAnnotation = field.getAnnotation(JSONField.class);

					if (fieldAnnotation != null && fieldAnnotation.name().length() != 0) {
						propertyName = fieldAnnotation.name();

						if (aliasMap != null) {
							propertyName = aliasMap.get(propertyName);
							if (propertyName == null) {
								continue;
							}
						}
					}
				}

				if (aliasMap != null) {
					propertyName = aliasMap.get(propertyName);
					if (propertyName == null) {
						continue;
					}
				}

				fieldInfoMap.put(propertyName, new FieldInfo(propertyName, method, field));
			}
		}

		for (final Field field : clazz.getFields()) {
			if (Modifier.isStatic(field.getModifiers())) {
				continue;
			}

			if (!Modifier.isPublic(field.getModifiers())) {
				continue;
			}

			if (!fieldInfoMap.containsKey(field.getName())) {
				fieldInfoMap.put(field.getName(), new FieldInfo(field.getName(), null, field));
			}
		}

		for (final FieldInfo fieldInfo : fieldInfoMap.values()) {
			fieldInfoList.add(fieldInfo);
		}

		return fieldInfoList;
	}
}
